module Sorcery
  module Controller
    def self.included(klass)
      klass.class_eval do
        include InstanceMethods

        Config.submodules.each do |mod|
          submodule_name = mod.to_s.split('_').map(&:capitalize).join
          include Submodules.const_get(submodule_name) if Submodules.const_defined?(submodule_name, false)
        end
      end
      Config.update!
      Config.configure!
    end

    module InstanceMethods # rubocop:disable Metrics/ModuleLength
      # To be used as before_action.
      # Will trigger auto-login attempts via the call to logged_in?
      # If all attempts to auto-login fail, the failure callback will be called.
      def require_login
        return if logged_in?

        if Config.save_return_to_url && request.get? && !request.xhr? && !request.format.json?
          session[:return_to_url] = request.url
        end

        send(Config.not_authenticated_action)
      end

      # Takes credentials and returns a user on successful authentication.
      # Runs hooks after login or failed login.
      def login(*credentials)
        @current_user = nil

        user_class.authenticate(*credentials) do |user, failure_reason|
          if failure_reason
            after_failed_login!(credentials)

            yield(user, failure_reason) if block_given?

            # FIXME: Does using `break` or `return nil` change functionality?
            # rubocop:disable Lint/NonLocalExitFromIterator
            return
            # rubocop:enable Lint/NonLocalExitFromIterator
          end

          old_session = session.dup.to_hash
          reset_sorcery_session
          old_session.each_pair do |k, v|
            session[k.to_sym] = v
          end

          auto_login(user, credentials[2])
          after_login!(user, credentials)

          block_given? ? yield(current_user, nil) : current_user
        end
      end

      def login!(...)
        user = login(...)

        raise Sorcery::InvalidCredentials if user.nil?

        user
      end

      def reset_sorcery_session
        reset_session # protect from session fixation attacks
      end

      # Resets the session and runs hooks before and after.
      def logout
        return unless logged_in?

        user = current_user
        before_logout!
        @current_user = nil
        reset_sorcery_session
        after_logout!(user)
      end

      def logged_in?
        !!current_user
      end

      # attempts to auto-login from the sources defined (session, basic_auth, cookie, etc.)
      # returns the logged in user if found, nil if not
      def current_user
        @current_user = login_from_session || login_from_other_sources || nil unless defined?(@current_user)
        @current_user
      end

      def current_user=(user)
        @current_user = user
      end

      # used when a user tries to access a page while logged out, is asked to login,
      # and we want to return him back to the page he originally wanted.
      def redirect_back_or_to(...)
        if Config.use_redirect_back_or_to_by_rails
          super
        else
          Sorcery.deprecator.warn(
            '`redirect_back_or_to` overrides the method of the same name defined in Rails 7. ' \
            'To avoid overriding, set `config.use_redirect_back_or_to_by_rails = true` and use `redirect_to_before_login_path`. ' \
            'In a future release, `config.use_redirect_back_or_to_by_rails = true` will become the default.'
          )
          redirect_to_before_login_path(...)
        end
      end

      def redirect_to_before_login_path(url, **options)
        allow_other_host = options[:allow_other_host].nil? ? _allow_other_host : options[:allow_other_host]
        flash = options.except(:allow_other_host)

        redirect_to(session[:return_to_url] || url, flash:, allow_other_host:)
        session[:return_to_url] = nil
      end

      # The default action for denying non-authenticated users.
      # You can override this method in your controllers,
      # or provide a different method in the configuration.
      def not_authenticated
        redirect_to root_path
      end

      # login a user instance
      #
      # @param [<User-Model>] user the user instance.
      # @return - do not depend on the return value.
      def auto_login(user, _should_remember = false)
        session[:user_id] = user.id.to_s
        @current_user = user
      end

      # Overwrite Rails' handle unverified request
      def handle_unverified_request
        cookies[:remember_me_token] = nil
        @current_user = nil
        super # call the default behaviour which resets the session
      end

      protected

      # Tries all available sources (methods) until one doesn't return false.
      def login_from_other_sources
        result = nil
        Config.login_sources.find do |source|
          result = send(source)
        end
        result || false
      end

      def login_from_session
        @current_user = (user_class.sorcery_adapter.find_by_id(session[:user_id]) if session[:user_id])
      end

      def after_login!(user, credentials = [])
        Config.after_login.each { |c| send(c, user, credentials) }
      end

      def after_failed_login!(credentials)
        Config.after_failed_login.each { |c| send(c, credentials) }
      end

      def before_logout!
        Config.before_logout.each { |c| send(c) }
      end

      def after_logout!(user)
        Config.after_logout.each { |c| send(c, user) }
      end

      def after_remember_me!(user)
        Config.after_remember_me.each { |c| send(c, user) }
      end

      def after_login_lock!(credentials)
        Config.after_login_lock.each { |c| send(c, credentials) }
      end

      def user_class
        @user_class ||= Config.user_class.to_s.constantize
      rescue NameError
        raise ArgumentError, 'You have incorrectly defined user_class or have forgotten to define it in the initializer file (config.user_class = \'User\').'
      end
    end
  end
end
